/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.runtime.config.spring.parsers.assembly.configuration;

import org.mule.runtime.config.spring.parsers.AbstractMuleBeanDefinitionParser;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.springframework.util.StringUtils;

/**
 * A direct implementation of {@link PropertyConfiguration}
 */
public class SimplePropertyConfiguration implements PropertyConfiguration {

  private List<String> references = new ArrayList<String>();
  private Properties nameMappings = new Properties();
  private Map<String, NamedValueMap> valueMappings = new HashMap<String, NamedValueMap>();
  private Set<String> collections = new HashSet<String>();
  private Map<String, Boolean> ignored = new HashMap<String, Boolean>();
  private boolean ignoreAll = false;

  @Override
  public void addReference(String propertyName) {
    references.add(dropRef(propertyName));
  }

  @Override
  public void addMapping(String propertyName, Map<String, Object> mappings) {
    valueMappings.put(propertyName, new NamedValueMap(propertyName, mappings));
  }

  @Override
  public void addMapping(String propertyName, String mappings) {
    valueMappings.put(propertyName, new NamedValueMap(propertyName, mappings));
  }

  @Override
  public void addMapping(String propertyName, ValueMap mappings) {
    valueMappings.put(propertyName, new NamedValueMap(propertyName, mappings));
  }

  @Override
  public void addAlias(String alias, String propertyName) {
    nameMappings.put(alias, propertyName);
  }

  @Override
  public void addCollection(String propertyName) {
    collections.add(dropRef(propertyName));
  }

  @Override
  public void addIgnored(String propertyName) {
    ignored.put(dropRef(propertyName), Boolean.TRUE);
  }

  @Override
  public void removeIgnored(String propertyName) {
    ignored.put(dropRef(propertyName), Boolean.FALSE);
  }

  @Override
  public void setIgnoredDefault(boolean ignoreAll) {
    this.ignoreAll = ignoreAll;
  }

  @Override
  public String getAttributeMapping(String alias) {
    return getAttributeMapping(alias, alias);
  }

  @Override
  public String getAttributeAlias(String name) {
    for (Iterator<?> iterator = nameMappings.entrySet().iterator(); iterator.hasNext();) {
      Map.Entry<?, ?> entry = (Map.Entry<?, ?>) iterator.next();
      if (entry.getValue().equals(name)) {
        return entry.getKey().toString();
      }
    }
    return name;
  }

  public String getAttributeMapping(String alias, String deflt) {
    return nameMappings.getProperty(alias, deflt);
  }

  @Override
  public boolean isCollection(String propertyName) {
    return collections.contains(dropRef(propertyName));
  }

  @Override
  public boolean isIgnored(String propertyName) {
    String name = dropRef(propertyName);
    if (ignored.containsKey(name)) {
      return ignored.get(name).booleanValue();
    } else {
      return ignoreAll;
    }
  }

  /**
   * A property can be explicitly registered as a bean reference via registerBeanReference() or it can simply use the "-ref"
   * suffix.
   * 
   * @param attributeName true if the name appears to correspond to a reference
   */
  @Override
  public boolean isReference(String attributeName) {
    return (references.contains(dropRef(attributeName))
        || attributeName.endsWith(AbstractMuleBeanDefinitionParser.ATTRIBUTE_REF_SUFFIX)
        || attributeName.endsWith(AbstractMuleBeanDefinitionParser.ATTRIBUTE_REFS_SUFFIX)
        || attributeName.equals(AbstractMuleBeanDefinitionParser.ATTRIBUTE_REF));
  }

  @Override
  public SingleProperty getSingleProperty(String name) {
    return new SinglePropertyWrapper(name, this);
  }

  /**
   * Extract a JavaBean property name from the supplied attribute name.
   * <p>
   * The default implementation uses the {@link org.springframework.core.Conventions#attributeNameToPropertyName(String)} method
   * to perform the extraction.
   * <p>
   * The name returned must obey the standard JavaBean property name conventions. For example for a class with a setter method
   * '<code>setBingoHallFavourite(String)</code>', the name returned had better be '<code>bingoHallFavourite</code>' (with that
   * exact casing).
   *
   * @param oldName the attribute name taken straight from the XML element being parsed; will never be <code>null</code>
   * @return the extracted JavaBean property name; must never be <code>null</code>
   */
  @Override
  public String translateName(String oldName) {
    // Remove the bean reference suffix if any.
    String name = dropRef(oldName);
    // Map to the real property name if necessary.
    name = getAttributeMapping(name);
    // JavaBeans property convention.
    name = Conventions.attributeNameToPropertyName(name);
    if (!StringUtils.hasText(name)) {
      throw new IllegalStateException("Illegal property name for " + oldName + ": cannot be null or empty.");
    }
    return name;
  }

  protected String dropRef(String name) {
    return org.mule.runtime.core.util.StringUtils
        .chomp(org.mule.runtime.core.util.StringUtils.chomp(name, AbstractMuleBeanDefinitionParser.ATTRIBUTE_REF_SUFFIX),
               AbstractMuleBeanDefinitionParser.ATTRIBUTE_REFS_SUFFIX);
  }

  @Override
  public Object translateValue(String name, String value) {
    NamedValueMap vm = valueMappings.get(name);
    if (vm != null) {
      return vm.getValue(value);
    } else {
      return value;
    }
  }


  public static class NamedValueMap {

    private String propertyName;
    private ValueMap valueMap;

    public NamedValueMap(String propertyName, String mappingsString) {
      this.propertyName = propertyName;
      valueMap = new MapValueMap(mappingsString);
    }

    public NamedValueMap(String propertyName, Map<String, Object> valueMap) {
      this.propertyName = propertyName;
      this.valueMap = new MapValueMap(valueMap);
    }

    public NamedValueMap(String propertyName, ValueMap valueMap) {
      this.propertyName = propertyName;
      this.valueMap = valueMap;
    }

    public String getPropertyName() {
      return propertyName;
    }

    public Object getValue(String key) {
      return valueMap.rewrite(key);
    }
  }

  public static class MapValueMap implements ValueMap {

    protected Map<String, Object> map;

    public MapValueMap(Map<String, Object> map) {
      this.map = map;
    }

    public MapValueMap(String definition) {
      map = new HashMap<String, Object>();

      String[] values = StringUtils.tokenizeToStringArray(definition, ",");
      for (int i = 0; i < values.length; i++) {
        String value = values[i];
        int x = value.indexOf("=");
        if (x == -1) {
          throw new IllegalArgumentException("Mappings string not properly defined: " + definition);
        }
        map.put(value.substring(0, x), value.substring(x + 1));
      }

    }

    @Override
    public Object rewrite(String value) {
      Object result = map.get(value);
      if (null == result) {
        return value;
      } else {
        return result.toString();
      }
    }

  }

  public static class IndentityMapValueMap extends MapValueMap {

    public IndentityMapValueMap(Map<String, Object> map) {
      super(map);
    }

    @Override
    public Object rewrite(String value) {
      Object result = map.get(value);
      return result;
    }
  }
}
